﻿// <copyright file="DataCollector.cs" company="Microsoft Corporation">
// Copyright (c) 2009 Microsoft Corporation All Rights Reserved
// </copyright>
// <author>Michael S. Scherotter</author>
// <email>mischero@microsoft.com</email>
// <date>2009-10-17</date>
// <summary>DataCollector class definition</summary>

namespace Microsoft.WebAnalytics
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Globalization;
    using System.IO.IsolatedStorage;
    using System.Net.NetworkInformation;
    using System.Reflection;
    using System.Windows;
#if WINDOWS_PHONE
#elif SILVERLIGHT 
    using System.Windows.Browser;
#endif
    using Microsoft.WebAnalytics.Contracts;

    /// <summary>
    /// Analytics Data collector
    /// </summary>
#if !WINDOWS_PHONE
    [Export(typeof(IDataCollector))]
#endif
    public class DataCollector : IDataCollector
    {
        #region Fields

        /// <summary>
        /// Isolated storage settings field name
        /// </summary>
        private const string IsEnabledSetting = "Microsoft.WebAnalytics.IsEnabled";

        /// <summary>
        /// Is the data collector enabled
        /// </summary>
        private static bool? isEnabled;

        /// <summary>
        /// The Title of the application from the assembly attributes or the Windows Phone App Manifest
        /// </summary>
        private string title;

        /// <summary>
        /// Has this been initialized yet?
        /// </summary>
        private bool initialized;

        /// <summary>
        /// The Application's initialization parameters
        /// </summary>
        private Dictionary<string, string> initParams;

        /// <summary>
        /// The start time for the application
        /// </summary>
        private DateTimeOffset startTime;

        /// <summary>
        /// the application's assembly version
        /// </summary>
        private string version;

#if SILVERLIGHT && !WINDOWS_PHONE
        /// <summary>
        /// the referrer URL
        /// </summary>
        private string referrer;

        /// <summary>
        /// the Document URI
        /// </summary>
        private Uri documentUri;
#endif

        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the DataCollector class
        /// </summary>
        public DataCollector()
        {
            this.startTime = DateTimeOffset.Now;

#if SILVERLIGHT && !WINDOWS_PHONE
            if (HtmlPage.IsEnabled)
            {
                this.documentUri = HtmlPage.Document.DocumentUri;

                this.referrer = HtmlPage.Document.GetProperty("referrer") as string;
            }
            else
            {
                // Out-of-browser application or HTML access is not enabled
                this.documentUri = Application.Current.Host.Source;
            }

            System.Windows.Interop.SilverlightHost host =
    Application.Current.Host;

            int version = 3;

            while (host.IsVersionSupported(version.ToString(CultureInfo.InvariantCulture) + ".0"))
            {
                version++;
            }

            this.SilverlightVersion = (version - 1).ToString(CultureInfo.InvariantCulture) + ".0";
#endif
            NetworkChange.NetworkAddressChanged += new NetworkAddressChangedEventHandler(this.NetworkChange_NetworkAddressChanged);
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets a value indicating whether to enable analytics collection
        /// </summary>
        /// <remarks>Use this setting to comply with privacy policies</remarks>
        public static bool IsEnabled
        {
            get
            {
                if (!isEnabled.HasValue)
                {
#if SILVERLIGHT
                    try
                    {
                        bool isEnabledValue = true;
                        if (IsolatedStorageSettings.ApplicationSettings.TryGetValue(IsEnabledSetting, out isEnabledValue))
                        {
                            isEnabled = isEnabledValue;
                        }
                        else
                        {
                            isEnabled = true;
                        }
                    }
                    catch (IsolatedStorageException)
                    {
                        isEnabled = false;
                    }
#else
                    isEnabled = Properties.Settings.Default.IsEnabled;
#endif
                }

                return isEnabled.Value;
            }

            set
            {
                isEnabled = value;

#if SILVERLIGHT
                try
                {
                    IsolatedStorageSettings.ApplicationSettings[IsEnabledSetting] = value;
                }
                catch (IsolatedStorageException)
                {
                }
#else
                Properties.Settings.Default.IsEnabled = value;

                Properties.Settings.Default.Save();
#endif
            }
        }
#if SILVERLIGHT
        /// <summary>
        /// Gets the major Silverlight Version installed
        /// </summary>
        public string SilverlightVersion { get; private set; }
#endif
        /// <summary>
        /// Gets or sets the event log
        /// </summary>
        [Import]
        public EventLog EventLog { get; set; }

        /// <summary>
        /// Gets or sets the event dispatcher
        /// </summary>
        [Import]
        public WebAnalyticsEvents EventDispatcher { get; set; }

        /// <summary>
        /// Gets or sets the A/B Testing variant
        /// </summary>
        [Import]
        public string ABTestingStateName { get; set; }
        #endregion

        /// <summary>
        /// Gets or sets the function to save the testing state to isolated storage
        /// </summary>
        [Import]
        public Action<string> SaveTestingState { get; set; }

        /// <summary>
        /// Gets or sets the title of the assembly or the title of the Windows Phone application
        /// </summary>
        protected string Title
        {
            get
            {
                if (string.IsNullOrEmpty(this.title))
                {
                    var titleAttribute = Attribute.GetCustomAttribute(Application.Current.GetType().Assembly, typeof(AssemblyTitleAttribute)) as AssemblyTitleAttribute;

                    if (titleAttribute != null)
                    {
                        this.title = titleAttribute.Title;
                    }
                }

                return this.title;
            }

            set
            {
                this.title = value;
            }
        }

        /// <summary>
        /// Gets or sets the version of the assembly or the version of the Windows Phone application
        /// </summary>
        protected string Version
        {
            get
            {
                if (string.IsNullOrEmpty(this.version))
                {
                    var assemblyName = new AssemblyName(Application.Current.GetType().Assembly.FullName);

                    if (assemblyName != null)
                    {
                        Version v = assemblyName.Version;

                        if (v != null)
                        {
                            this.version = v.ToString();
                        }
                    }
                }

                return this.version;
            }

            set
            {
                this.version = value;
            }
        }

        #region Methods

        /// <summary>
        /// Initialize the data collector with the application's initialization parameters
        /// </summary>
        /// <param name="dictionary">the initialization parameters from the application startup event handler</param>
        public void Initialize(System.Collections.Generic.Dictionary<string, string> dictionary)
        {
            if (!this.initialized)
            {
                if (dictionary.ContainsKey("SAFVariant"))
                {
                    var variant = dictionary["SAFVariant"];

                    if (variant == "control")
                    {
                        this.CommitStateName(null);
                    }
                    else
                    {
                        this.CommitStateName(variant);
                    }
                }

                this.initParams = dictionary;

                this.DispatchCachedEvents();

                this.initialized = true;
            }

            Application.Current.Host.NavigationStateChanged += new EventHandler<System.Windows.Interop.NavigationStateChangedEventArgs>(this.Host_NavigationStateChanged);

            this.OnInitialize();
        }

        /// <summary>
        /// Dispatch the events to the service(s) or log them to the event log.
        /// </summary>
        /// <param name="logEvent">the event to log</param>
        /// <exception cref="ArgumentNullException">if the logEvent is null.</exception>
#if !WINDOWS_PHONE
        [Export("Log")]
#endif
        public void Log(AnalyticsEvent logEvent)
        {
            if (logEvent == null)
            {
                throw new ArgumentNullException("logEvent");
            }

            if (!IsEnabled)
            {
                return;
            }

            if (!this.initialized)
            {
                this.DispatchCachedEvents();

                this.initialized = true;
            }

#if SILVERLIGHT
            var host = Application.Current.Host;
            var content = host.Content;
            logEvent.Resolution   = new Size(content.ActualWidth, content.ActualHeight);
#if !WINDOWS_PHONE
            logEvent.IsFullScreen = content.IsFullScreen;
            logEvent.ZoomFactor   = content.ZoomFactor;
            logEvent.InstallState = Application.Current.InstallState;
#endif // !WINDOWS_PHONE

            // If a NavigationState wasn't specified then put it into the NavigationState property
            if (string.IsNullOrEmpty(logEvent.NavigationState))
            {
                var navigationState = string.Empty;

                try
                {
                    navigationState = host.NavigationState;
                }
                catch
                {
                }

                logEvent.NavigationState = navigationState;
            }

            logEvent.SilverlightVersion = this.SilverlightVersion;
#endif // SILVERLIGHT
            logEvent.Language = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
            logEvent.AppName = this.Title;
            logEvent.Version = this.Version;
            logEvent.Timestamp = DateTimeOffset.Now;
            logEvent.TimeCode = logEvent.Timestamp - this.startTime;
            logEvent.StateName = this.ABTestingStateName;

            if (this.initParams != null)
            {
                foreach (var initParam in this.initParams)
                {
                    logEvent.EventArgs.Add(new Microsoft.WebAnalytics.Data.PropertyValue() { PropertyName = "i." + initParam.Key, Value = initParam.Value });
                }
            }

#if WINDOWS_PHONE
#elif SILVERLIGHT
            if (HtmlPage.IsEnabled)
            {
                try
                {
                    logEvent.UserAgent = HtmlPage.BrowserInformation.UserAgent;
                }
                catch (InvalidOperationException)
                {
                    // Google Chrome will not return the user agent when exiting.
                }
            }
            else
            {
                logEvent.UserAgent = "Silverlight";
            }

            if (!string.IsNullOrEmpty(this.referrer))
            {
                logEvent.Referrer = this.referrer;
            }

            logEvent.DocumentUri = this.documentUri;
#else
            logEvent.UserAgent = "WPF";
#endif
            this.OnLog(logEvent);

            if (NetworkInterface.GetIsNetworkAvailable())
            {
                if (!this.EventDispatcher.DispatchEvents(logEvent, false))
                {
                    this.EventLog.WriteEntry(logEvent);
                }
            }
            else
            {
                this.EventLog.WriteEntry(logEvent);
            }
        }

        /// <summary>
        /// Prepare an analytics event for logging
        /// </summary>
        /// <param name="logEvent">an analytics log event</param>
        /// <remarks>It is not necessary to call the base class (DataCollector.OnLog does nothing.)
        /// You should override this method to log additional device- or OS-specific data.</remarks>
        protected virtual void OnLog(AnalyticsEvent logEvent)
        {
        }

        /// <summary>
        /// Override to provide device- or OS-specific initialization
        /// </summary>
        protected virtual void OnInitialize()
        {
        }

        #endregion

        #region Implementation

        /// <summary>
        /// Handle navigation state changed events
        /// </summary>
        /// <param name="sender">the SilverlightHost</param>
        /// <param name="e">the navigation state changed event arguments</param>
        private void Host_NavigationStateChanged(object sender, System.Windows.Interop.NavigationStateChangedEventArgs e)
        {
            var logEvent = new AnalyticsEvent
            {
                HitType = Data.HitType.PageView,
                Name = "NavigationStateChanged",
                ObjectType = sender.GetType().Name
            };

            this.Log(logEvent);
        }

        /// <summary>
        /// Network address changed event handler
        /// </summary>
        /// <param name="sender">the Network Change</param>
        /// <param name="e">the event arguments</param>
        private void NetworkChange_NetworkAddressChanged(object sender, EventArgs e)
        {
            this.DispatchCachedEvents();
        }

        /// <summary>
        /// Dispatch the cached events
        /// </summary>
        private void DispatchCachedEvents()
        {
            if (NetworkInterface.GetIsNetworkAvailable())
            {
                if (!IsEnabled)
                {
                    return;
                }

                var events = this.EventLog.GetEvents();

                if (events == null)
                {
                    return;
                }

                foreach (var logEvent in events)
                {
                    this.EventDispatcher.DispatchEvents(logEvent, true);
                }
            }
        }

        /// <summary>
        /// Commit the state name
        /// </summary>
        /// <param name="variant">the state name</param>
        private void CommitStateName(string variant)
        {
            this.ABTestingStateName = variant;

            this.SaveTestingState(variant);
        }

        #endregion
    }
}
